Java-8-17 新特性

背景介绍

截止目前为止,JDK比较重要的时间节点和版本说明:

  • 1996 JDK1.0
  • 2004 JDK5.0 最重要的一个里程碑式的版本
  • 2014 JDK8.0 排第二的里程碑式的版本 LTS
  • 2017.9 JDK9.0 从此版本开始,每半年发布一个新的版本
  • 2018.9 JDK11 LTS
  • 2021.9 JDK17 LTS


如何学习一些新特性呢?

  • 新的语法规则:自动拆箱和装箱、注解、enum、Lambda表达式、方法引用、switch表达式升级、try-with、record 等等;
  • 增加、过时、删除 API:StringBuilder、ArrayList、新的日期时间的API、Optional 等。
  • 底层的优化以及JVM参数的调整:GC的变化、内存结构(永久代–>元空间)。


Lambda 表达式(8+)

面向过程额面向对象编程

Java从诞生之日起就一直提倡“一切皆对象”,在java中面向对象编程(OOP)就是一切。但是随着 python、scala 等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的级数要求,即java不但可以支持OOP,还可以支持OOF(面向过程编程)。

  • Java8 引入了 Lambda 表达式之后,Java也开始支持函数式编程。
  • Lambda 不是最早使用的,目前 C++、C#、Python、Scala 等均支持 Lambda 表达式。

简单地说,面向对象的思想就是 “做一件事情,找一个能解决事情的对象,调用对象的方法,完成这件事”。函数式编程思想是 “只要能获取到结果,谁去做的,怎么做到都不重要,重视的是结果而非过程”。在函数式编程的语言中,函数被当做 一等公民对待。在将函数作为一等公民的编程语言当中,lambda 表达式的类型是函数。但是在 java8中有所不同。在java8中,lambda表达式是对象,而非函数,它们必须依附于一类特别的对象类型——函数式接口。Java8 中 Lambda表达式就是一个函数式接口(接口中只声明了一个抽象方法)的实例,只要一个对象是函数式接口的实例,那么该对象就可以用lambda表达式来表示。

Lambda表达式的本质:

  • 一方面 lambda表达式作为接口的实现类的对象。
  • 另一方面,lambda表达式是一个匿名函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionalInterface  // 标识一个函数式接口
public interface Runnable {
public abstract void run();
}

@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);

boolean equals(Object obj); // Object 中已实现

default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
// ...
}


四大核心函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 消费型接口
*/
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); ////

default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}

/**
* 供给型接口
*/
@FunctionalInterface
public interface Supplier<T> {
T get(); ////
}

/**
* 函数式接口
*/
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); ////

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

static <T> Function<T, T> identity() {
return t -> t;
}
}

/**
* 判断型接口
*/
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); ////

default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

default Predicate<T> negate() {
return (t) -> !test(t);
}

default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}

static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}


Lambda 的一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Test
public void test01() {
Runnable r1 = new Runnable() { // Runnable 接口中只有一个抽象方法
@Override
public void run() {
System.out.println("aaaa");
}
};
// 可以简化为
Runnable r2 = () -> System.out.println("aaaa");

new Thread(r2).start();
}

@Test
public void test02() {
Consumer<String> c1 = new Consumer<>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
// 可以简化为
Consumer<String> c2 = (String s) -> System.out.println(s);
Consumer<String> c3 = s -> System.out.println(s); // 类型是可以推断出来的
Consumer<String> c4 = System.out::println;

c4.accept("aaaa");
}

@Test
public void test03() {
Comparator<Integer> c1 = (o1, o2) -> {
return o1.compareTo(o2);
};
// 可以简化为
Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);
Comparator<Integer> c3 = Integer::compareTo;

int result = c3.compare(1, 2);
System.out.println(result);
}


方法引用和构造器引用(8+)

方法引用可以看做是 Lambda 表达式的进一步刻画。当需要提供一个函数式接口的实例时,我们可以使用 Lambda 表达式提供此实例。当满足一定的条件的情况下,我们还可以使用方法引用或构造器引用来替换 Lambda 表达式。方法引用的本质还是函数式接口的一个实例。(仅仅牢记这一点——在Java中万事万物皆对象)。方法引用存在以下三种格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 【对象::实例方法】
// 要求:函数式接口中的方法a与其内部实现时调用的对象的某个方法b的形参列表和返回值类型都兼容,此时可以考虑
// 使用方法b实现对方法a的替换,此替换即为方法引用。(注意此处的方法b是非静态的方法,需要使用对象来调用)
Consumer<String> c3 = s -> System.out.println(s);
Consumer<String> c4 = System.out::println;

User user = new User(1L, "zhangsan");
Supplier<String> s1 = () -> user.getName();
Supplier<String> s2 = user::getName;


// 【类::静态方法】
// 要求:函数式接口中的方法a与其内部实现时调用的类的某个静态方法b的形参列表和返回值类型都兼容,此时可以考虑
// 使用方法b实现对方法a的替换,此替换即为方法引用。
Function<Double, Long> f1 = new Function<Double, Long>() {
@Override
public Long apply(Double d) {
return Math.round(d);
}
};
Function<Double, Long> f2 = d -> Math.round(d);
Function<Double, Long> f3 = Math::round;


// 【类::实例方法】
// 要求:函数式接口中的方法a与其内部实现时调用的对象的某个方法b的返回值类型相同,同时抽象方法a中有n个参数,
// 方法b中有n-1个参数,且抽象方法a的第一个参数作为方法b的调用者,且抽象方法a的后n-1个参数与方法b的n-1个
// 参数类型兼容,则可以考虑使用方法b实现对方法a的替换,此替换即为方法引用。
// 注意,此方法b是非静态的方法,需要对象调用,但在形式上使用的是 “a所属的类::实例方法”。
Comparator<Integer> c1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
};
Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);
Comparator<Integer> c3 = Integer::compareTo;

构造器引用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static class User {
int id;

public User(){}
public User(int id) {
this.id = id;
}
}

@Test
public void test05() {
Function<Integer, User> f1 = new Function<Integer, User>() {
@Override
public User apply(Integer id) {
return new User(id);
}
};
Function<Integer, User> f2 = User::new; // 调用的是参数相匹配的构造器(取决于函数式接口的抽象方法的形参列表)
}

@Test
public void test06() {
Function<Integer, User[]> f1 = new Function<Integer, User[]>() {
@Override
public User[] apply(Integer length) {
return new User[length];
}
};
Function<Integer, User[]> f2 = User[]::new; // 数组引用
}


强大的 Stream API(8+)

Java8中最重要的改变,第一个是 Lambda 表达式,另一个则是 Stream API。Stream API(java.util.stream) 把真正的函数式编程风格引入到了java中,这是目前为止对java类库最好的补充,因为 stream API 可以极大提高程序员的生产力,让程序员写出高效、干净、简洁的代码。

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用 Stream API 对集合进行操作,就类似于使用 SQL 执行的数据库查询。怎么来理解 Stream 呢?实际上你可以把 Stream 看作是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,讲的是数据,它关注的是数据的存储,是面向内存的;而 Stream 是有关计算的,讲的是计算(排序、查找、过滤、映射、遍历等),是面向CPU的。需要注意的是:

  • Stream 不会自己存储元素;
  • Stream 不会改变源对象,它只会返回一个持有结果的新 Stream。
  • Stream 的操作是延迟执行的,这意味着它们会等到需要结果的时候才执行,即一旦执行终止操作,就执行中间操作链,并产生结果。
  • Stream 一旦执行了终止操作,就不能调用其他中间操作或终止操作了。


Stream 的操作三个步骤

一、创建Stream

通过一个数据源(如 集合、数组)获取一个流。

二、中间操作

每次操作都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。因此中间操作可以是操作链,可对数据进行n次处理,但是在中介操作之前,并不会真正去执行。

三、终止操作

终止操作方法的返回值不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束Stream。


创建 Stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 创建方式1:通过集合 Collection#stream 或 parallelStream
*/
@Test
public void test01() {
List<String> list = Arrays.asList("AA", "BB", "CC", "DD", "EE");
Stream<String> stream1 = list.stream(); // 返回一个顺序流
Stream<String> stream2 = list.parallelStream(); // 返回一个并行流
System.out.println(stream1.getClass()); // class java.util.stream.ReferencePipeline$Head
System.out.println(stream2.getClass()); // class java.util.stream.ReferencePipeline$Head
}

/**
* 创建方式2:通过数组 Arrays.stream
*/
@Test
public void test02() {
Integer[] array1 = {1,2,3,4,5};
Stream<Integer> stream1 = Arrays.stream(array1);
System.out.println(stream1); // java.util.stream.ReferencePipeline$Head@3d680b5a

int[] array2 = {1,2,3,4,5};
IntStream stream2 = Arrays.stream(array2);
System.out.println(stream2); // java.util.stream.IntPipeline$Head@61230f6a
}

/**
* 创建方式3:通过 Stream.of
*/
@Test
public void test03() {
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
System.out.println(stream); // java.util.stream.ReferencePipeline$Head@ef9296d
}


中间操作举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* 筛选与切片
*/
@Test
public void test01() {
// filter(Predicate p):查询薪资大于7000的员工
empList.stream()
.filter(emp -> emp.getSalary() > 7000)
.forEach(System.out::println); // 终止操作
System.out.println();

// limit(n):截断使元素不超过给定的数量
empList.stream()
.limit(2)
.forEach(System.out::println);
System.out.println();

// skip(n):跳过元素,返回一个扔掉了前n个元素的流,若流中的元素不足n,则返回一个空的stream
empList.stream()
.skip(2)
.forEach(System.out::println);
System.out.println();

// distinct():筛选,通过流所生成的元素的 hashCode 和 equals 去除重复的元素
empList.stream()
.distinct()
.forEach(System.out::println);
System.out.println();
}

/**
* Java 9+ 新增切片操作(有顺序的流中效果最明显)
*/
@Test
public void test02() {
List<Integer> list = List.of(1, 2, 3, 4, 5, 1, 2);

// 1. takeWhile(Predicate p):从开头开始取,直到“不满足条件”时立即断开,哪怕后面还有满足条件的元素
list.stream()
.takeWhile(n -> n < 4) // 只要小于4就拿走
.forEach(System.out::println); // 输出: 1, 2, 3
System.out.println();

// 2. dropWhile(Predicate p):与 takeWhile 相反,从开头开始删,直到“不满足条件”时停止删除,保留后续所有元素
list.stream()
.dropWhile(n -> n < 4) // 只要小于4就扔掉,一旦遇到4,后面全保留
.forEach(System.out::println); // 输出: 4, 5, 1, 2
}

/**
* 映射
*/
@Test
public void test03() {
// map(Function f)
Stream.of("a", "b", "c")
.map(String::toUpperCase)
.forEach(System.out::println);
}

/**
* 排序
*/
@Test
public void test04() {
// sorted():自然排序
Integer[] array = {121, 12, 1, 2, 3, 4, 43};
Arrays.stream(array)
.sorted() // 被排序的元素必须实现 Comparable 接口
.forEach(System.out::println);
System.out.println();

// sorted(Comparator c):定制排序
Arrays.stream(array)
.sorted((o1, o2) -> - o1.compareTo(o2))
.forEach(System.out::println);
}

/**
* 映射与扁平化(补充)
*/
@Test
public void test05() {
// 1. flatMap(Function f):扁平化映射
// 将流中的每个元素都转换成另一个流,然后把所有这些流连接成一个流
Stream.of("hello", "world")
.flatMap(str -> Arrays.stream(str.split(""))) // 将每个字符串拆成字母流,再合并
.forEach(System.out::print); // 输出: helloworld
System.out.println();

// 2. mapToInt / mapToLong / mapToDouble:转换成原始类型流(避免自动装箱)
// 对应的还有 flatMapToInt, flatMapToLong, flatMapToDouble
empList.stream()
.mapToInt(Emp::getAge) // 返回 IntStream,拥有 sum(), average() 等特有方法
.forEach(System.out::println);
}

/**
* 消费/调试
*
* 过滤后的员工: 张三
* 张三
* 过滤后的员工: 李四
* 李四
* 过滤后的员工: 王五
* 王五
*/
@Test
public void test06() {
// peek(Consumer action):对每个元素执行操作,并返回一个包含原元素的新流
// 常用于在不影响流后续操作的情况下,观察流内部的元素状态(Debug 调试神器)
empList.stream()
.filter(emp -> emp.getSalary() > 7000)
.peek(emp -> System.out.println("过滤后的员工: " + emp.getName())) // 调试打印
.map(Emp::getName)
.forEach(System.out::println);
}


终止操作举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* 匹配与查找
*/
@Test
public void test201() {
boolean result1 = empList.stream()
.allMatch(emp -> emp.getAge() > 18);
System.out.println(result1);

boolean result2 = empList.stream()
.anyMatch(emp -> emp.getAge() > 18);
System.out.println(result2);

Optional<Emp> firstOptional = empList.stream()
.findFirst();// findAny
System.out.println(firstOptional.get());
}

/**
* 简单统计与遍历
*/
@Test
public void test202() {
// count
long count = empList.stream().count();
System.out.println(count);
System.out.println();

// max(Comparator c),min(Comparator c)
Optional<Emp> maxOptional = empList.stream().max(Emp::compareTo);
System.out.println(maxOptional.get());
System.out.println();

// forEach(Consumer c)
empList.stream().forEach(System.out::println);
empList.forEach(System.out::println);

// 2. forEachOrdered(Consumer action)
// 严格按照流的遭遇顺序(Encounter Order)进行遍历。
// 在串行流中与 forEach 没区别,但在【并行流(parallelStream)】中,
// forEach 的输出是乱序的,而 forEachOrdered 能保证输出顺序与原集合一致。
empList.parallelStream().forEachOrdered(System.out::println);
}

/**
* 归约
*/
@Test
public void test203() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer result = list.stream().reduce(0, Integer::sum); // reduce(T identity, BiFunction<T> bf)
System.out.println(result); // 55
}

/**
* 数值统计
*/
@Test
public void test206() {
// 1. sum():求和(返回基本类型,无需像 reduce 那样装箱)
Double totalSalary = empList.stream().mapToDouble(Emp::getSalary).sum();
System.out.println("总薪资:" + totalSalary);

// 2. average():求平均值(因为流可能为空,返回 OptionalDouble)
OptionalDouble avgAge = empList.stream().mapToInt(Emp::getAge).average();
avgAge.ifPresent(avg -> System.out.println("平均年龄:" + avg));

// 3. summaryStatistics():终极统计大礼包
// 只需遍历一次流,就能同时获取 count, sum, min, max, average
DoubleSummaryStatistics stats = empList.stream().mapToDouble(Emp::getSalary).summaryStatistics();
System.out.println("最高薪资: " + stats.getMax());
System.out.println("平均薪资: " + stats.getAverage());
}

/**
* 收集
*/
@Test
public void test205() {
List<Emp> empList1 = empList.stream()
.filter(emp -> emp.getSalary() > 7000)
.collect(Collectors.toList());
empList1.forEach(System.out::println);
System.out.println();

List<Emp> empList2 = empList.stream()
.sorted(Comparator.comparingInt(Emp::getAge))
.collect(Collectors.toList());
empList2.forEach(System.out::println);

LinkedList<Emp> empList3 = empList.stream()
.collect(Collectors.toCollection(LinkedList::new));
empList3.forEach(System.out::println);

// toMap(Function keyMapper, Function valueMapper):转成 Map
// 注意:如果 Key 重复会抛异常,实际开发中常用第三个参数 (oldVal, newVal) -> newVal 来处理冲突
Map<Integer, String> idToNameMap = empList.stream()
.distinct()
.collect(Collectors.toMap(Emp::getId, Emp::getName));

// joining():字符串拼接
// 可传入 (分隔符, 前缀, 后缀)
String namesStr = empList.stream()
.map(Emp::getName)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(namesStr); // 输出如: [张三, 李四, 王五]

// groupingBy(Function classifier):分组(极为常用,相当于 SQL 的 group by)
// 返回 Map<分组键, List<元素>>
Map<String, List<Emp>> deptEmpMap = empList.stream()
.collect(Collectors.groupingBy(Emp::getDept));
System.out.println(deptEmpMap);

// partitioningBy(Predicate p):分区(特殊的分组)
// 根据断言(true/false)将流分成两部分,返回 Map<Boolean, List<元素>>
Map<Boolean, List<Emp>> salaryPartition = empList.stream()
.collect(Collectors.partitioningBy(emp -> emp.getSalary() > 7000));
System.out.println(salaryPartition);
}


Jshell(9+)

命令行中通过 jshell 指令,调取 jshell 工具(类似于浏览器对js的支持,或者python控制台编码)。常用的指令如下:

  • /help
  • /list:列出当前 session 里所有的有效的代码片段
  • /vars:查看当前 session 下的所有创建过的变量
  • /methods:查看当前 session 下的所有创建过的方法
  • /imports:列出导入的包
  • /history:键入内容的历史记录
  • /edit:使用外部代码编辑器来编写 java 代码
  • /exit:退出 jshell 工具


try-with 新特性(7+)

所有实现了 AutoCloseable 的接口都可以使用以下方式进行优雅地资源释放。

1
2
3
public interface AutoCloseable {
void close() throws Exception;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void test001() {
FileWriter fw = null;
BufferedWriter bw = null;
try {
fw = new FileWriter("test.txt");
bw = new BufferedWriter(fw);
bw.write("hello world");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

@Test
public void test002() {
try (FileWriter fw = new FileWriter("test.txt");
BufferedWriter bw = new BufferedWriter(fw)){
bw.write("hello world");
} catch (IOException e) {
e.printStackTrace();
}
}


类型推断(10+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 适合使用的场景
*/
@Test
public void testVar() {
// 1. 局部变量的实例化
var list = new ArrayList<String>();
var set = new LinkedHashSet<Integer>();

// 2. 增强 for 循环中的索引
for (var v : list) {
System.out.println(v);
}

// 3. 传统 for 循环中
for (var i = 0; i < 100; i++) {
System.out.println(i);
}

// 4. 返回值类型含复杂的泛型结构
Map<String, Integer> mm = new HashMap<>();
Set<Map.Entry<String, Integer>> entries1 = mm.entrySet();
var entries2 = mm.entrySet();
}


自动匹配类型(14+)

1
2
3
4
5
6
7
8
9
10
// 14之前
if (obj instanceof String) {
String s = (String) obj;
// ...
}

// 14+
if (obj instanceof String s) {
// ...
}


switch 表达式(12+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
public void test31() {
Week day = Week.SUNDAY;
switch (day) {
case MONDAY:
System.out.println(1);
break;
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
System.out.println(2);
break;
case SATURDAY:
case SUNDAY:
System.out.println(4);
break;
default:
throw new RuntimeException("参数非法");
}
}

@Test
public void test32() {
Week day = Week.SUNDAY;
int result = switch (day) {
case MONDAY -> { // 箭头操作符
yield 1;
} // yield 类似于 return,专门用于返回 switch 的匹配结果(如果有返回值的情况),也可以像下面的方式省略 yield 的写法
case TUESDAY, WEDNESDAY, THURSDAY -> 2;
case SATURDAY, SUNDAY -> 4;
default -> throw new RuntimeException("参数非法");
};
System.out.println(result);
}


多行文本(14+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test010(){
String s1 = """
<html>
<head>
</head>
<body>
<div>haha</div>
</body>
</html>
""";
System.out.println(s1);

String s2 = """
select id,name,age \
from t_user u \
where id > 4 \
order by id desc
""";
System.out.println(s2);
}


密封类(17+)

1
2
3
4
5
6
7
8
// Person 是一个密封类,可以被指定的类所继承,非指定的类不能继承 Person 类
sealed class Person permits Student, Teacher {}

// 要求子类必须是 final 的,或者子类也是 sealed 类(密封类)
final class Student extends Person {}
sealed class Teacher extends Person permits MathTeacher {}

final class MathTeacher extends Teacher {}


Record 类(16+)

在 Java 14 作为预览特性引入、并在 Java 16 正式转正的 Record(记录类型),是 Java 近年来最受欢迎的特性之一。它的核心目的非常纯粹:消除那些为了单纯“传递数据”而不得不写的海量样板代码,让 Java 的数据载体类(POJO / DTO)变得极其干净。在没有 Record 之前,如果你想写一个只有只读属性的 Point(坐标点)类,你需要写:

  • 私有字段 (private final)
  • 构造方法
  • 所有的 Getter 方法
  • toString() 方法
  • equals() 和 hashCode() 方法

即便用 IDE 自动生成或使用 Lombok 插件,代码依然臃肿。而使用 Record,只需要一行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 传统的 POJO 写法
public final class OldPoint {
private final int x;
private final int y;

public OldPoint(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() { return x; }
public int getY() { return y; }

@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}

// Java Record 写法(完全等价于上方 20+ 行代码)
public record Point(int x, int y) {}

当你声明一个 public record Point(int x, int y) {} 时,Java 编译器在底层会自动为你做以下事情:

  • 不可变性 (Immutable):每个属性在底层都是 private final 的。
  • 特殊的 Getter:生成的方法名没有 get 前缀,直接和属性名一致。例如:point.x() 和 point.y()。
  • 自带三大件:自动重写了规范的 equals()、hashCode() 和 toString()(格式为 Point[x=1, y=2])。
  • 继承限制:Record 隐式继承自 java.lang.Record。因为 Java 是单继承,所以 Record 不能再继承其他类,但可以实现接口。同时,Record 类本身是 final 的,不能被其他类继承。

Record 绝对不仅是一个“死板”的数据容器,它支持自定义构造器、实例方法和静态属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 员工数据的 Record 示例
* 展示了:自定义校验、紧凑构造器、实例方法、静态变量、实现接口
*/
public record Emp(Long id, String name, double salary) implements Serializable {
// 1. 静态变量与静态方法(允许存在)
public static final double MIN_SALARY = 3000.0;

// 2. 紧凑构造器(Compact Constructor):没有参数列表,专门用来做参数校验或转换
public Emp {
// 自动入参校验
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("员工姓名不能为空");
}
if (salary < MIN_SALARY) {
salary = MIN_SALARY; // 修改入参,最后编译器会自动将其赋值给 final 字段
}
// 注意:这里绝对不能写 this.name = name;
// 编译器在底层会自动在最末尾帮你加上所有的赋值语句!
}

/**
上面的紧凑构造器代码,在编译后生成的 .class 字节码其实等价于:

// 编译器在底层默默为你生成的实际代码:
public User(String name, int age) {
// 第一步:把你在紧凑构造器里写的校验/修改逻辑放进来
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名字不能为空");
}
if (age < 3000.0) {
age = 3000.0;
}

// 第二步:编译器自动在末尾追加强有力的赋值语句
this.name = name;
this.age = age;
}
*/

// 3. 定制构造器(Custom Constructor):允许提供不同参数个数的构造器
public Emp(String name) {
this(0L, name, MIN_SALARY); // 必须在第一行调用 Record 的主构造器
}

// 4. 自定义实例方法
public boolean isHighEarner() {
return this.salary > 10000;
}
}

测试使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testRecord() {
// 创建对象
Emp emp1 = new Emp(1L, "张三", 12000.0);
Emp emp2 = new Emp("李四"); // 使用定制构造器

// 1. 属性访问(注意没有 get 前缀)
System.out.println(emp1.name()); // 输出: 张三
System.out.println(emp2.salary()); // 输出: 3000.0 (触发了紧凑构造器的保底)

// 2. 自动生成的 toString()
System.out.println(emp1); // 输出: Emp[id=1, name=张三, salary=12000.0]

// 3. 自动生成的 equals 和 hashCode
Emp emp3 = new Emp(1L, "张三", 12000.0);
System.out.println(emp1.equals(emp3)); // 输出: true

// 4. 自定义方法调用
System.out.println(emp1.isHighEarner()); // 输出: true
}

Record 非常适合以下场景:

  • DTO(数据传输对象):RPC 调用、Controller 接收或返回的各种 Request/Response 载体。
  • 数据库映射(部分情况):如像 MyBatis / JdbcTemplate 查询出来的只读结果集。
  • 临时数据组合:在 Stream 流处理过程中,如果需要同时传递两个关联值,可以定义一个局部(Local)Record 快速打包。
  • 注意:Record 不适合用作传统的 Hibernate/JPA 实体类(Entity)。因为 JPA 实体通常依赖代理、延迟加载,且强烈需要可变的 setter 方法,这与 Record 的强不可变性背道而驰。